/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 * @format
 * @oncall react_native
 */

import type {BabelCoreOptions, BabelFileMetadata} from '@babel/core';

import {parseSync, transformFromAstSync} from '@babel/core';
import nullthrows from 'nullthrows';

export type CustomTransformOptions = {
  [string]: mixed,
  __proto__: null,
  ...
};

export type TransformProfile = 'default' | 'hermes-stable' | 'hermes-canary';

type BabelTransformerOptions = $ReadOnly<{
  customTransformOptions?: CustomTransformOptions,
  dev: boolean,
  enableBabelRCLookup?: boolean,
  enableBabelRuntime: boolean | string,
  extendsBabelConfigPath?: string,
  experimentalImportSupport?: boolean,
  hermesParser?: boolean,
  minify: boolean,
  platform: ?string,
  projectRoot: string,
  publicPath: string,
  unstable_transformProfile?: TransformProfile,
  globalPrefix: string,
  inlineRequires?: void,
  ...
}>;

export type BabelTransformerArgs = $ReadOnly<{
  filename: string,
  options: BabelTransformerOptions,
  plugins?: BabelCoreOptions['plugins'],
  src: string,
}>;

export type BabelFileFunctionMapMetadata = $ReadOnly<{
  names: $ReadOnlyArray<string>,
  mappings: string,
}>;

export type BabelFileImportLocsMetadata = ReadonlySet<string>;

export type MetroBabelFileMetadata = {
  ...BabelFileMetadata,
  metro?: ?{
    functionMap?: ?BabelFileFunctionMapMetadata,
    unstable_importDeclarationLocs?: ?BabelFileImportLocsMetadata,
    ...
  },
  ...
};

export type BabelTransformer = $ReadOnly<{
  transform: BabelTransformerArgs => $ReadOnly<{
    ast: BabelNodeFile,
    // Deprecated, will be removed in a future breaking release. Function maps
    // will be generated by an input Babel plugin instead and written into
    // `metadata` - transformers don't need to return them explicitly.
    functionMap?: BabelFileFunctionMapMetadata,
    metadata?: MetroBabelFileMetadata,
    ...
  }>,
  getCacheKey?: () => string,
}>;

function transform({
  filename,
  options,
  plugins,
  src,
}: BabelTransformerArgs): ReturnType<BabelTransformer['transform']> {
  const OLD_BABEL_ENV = process.env.BABEL_ENV;
  process.env.BABEL_ENV = options.dev
    ? 'development'
    : process.env.BABEL_ENV || 'production';

  try {
    const babelConfig: BabelCoreOptions = {
      ast: true,
      babelrc: options.enableBabelRCLookup,
      caller: {bundler: 'metro', name: 'metro', platform: options.platform},
      // NOTE(EvanBacon): We split the parse/transform steps up to accommodate
      // Hermes parsing, but this defaults to cloning the AST which increases
      // the transformation time by a fair amount.
      // You get this behavior by default when using Babel's `transform` method directly.
      cloneInputAst: false,
      code: false,
      cwd: options.projectRoot,
      filename,
      highlightCode: true,
      plugins,
      sourceType: 'module',
    };
    const sourceAst = options.hermesParser
      ? // eslint-disable-next-line import/no-commonjs
        require('hermes-parser').parse(src, {
          babel: true,
          sourceType: babelConfig.sourceType,
        })
      : parseSync(src, babelConfig);

    const transformResult = transformFromAstSync<MetroBabelFileMetadata>(
      // $FlowFixMe[incompatible-type] BabelFile vs BabelNodeFile
      sourceAst,
      src,
      babelConfig,
    );

    return {
      ast: nullthrows(transformResult.ast),
      metadata: transformResult.metadata,
    };
  } finally {
    if (OLD_BABEL_ENV) {
      process.env.BABEL_ENV = OLD_BABEL_ENV;
    }
  }
}

// Type check exports
/*::
({transform}) as BabelTransformer;
*/

export {transform};

/**
 * Backwards-compatibility with CommonJS consumers using interopRequireDefault.
 * Do not add to this list.
 *
 * @deprecated Default import from 'metro-babel-transformer' is deprecated, use named exports.
 */
export default {transform};
